﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;

namespace Beaker
{
	/// <summary>
	/// A maybe that can either has a value or not.
	/// </summary>
	/// <typeparam name="TValue">The type of the value the maybe could hold.</typeparam>
	public struct Maybe<TValue>
		: IMaybe, IEquatable<Maybe<TValue>>, IEquatable<TValue>
	{
		/// <summary>
		/// The Maybe without a value (nothing).
		/// </summary>
		public static readonly Maybe<TValue> Nothing = new Maybe<TValue>();

		private readonly bool _hasValue;
		private readonly TValue _value;

		/// <summary>
		/// Create a new instance of a maybe with the given value
		/// </summary>
		/// <param name="value">The value of the maybe.</param>
		public Maybe(TValue value)
		{
			_hasValue = true;
			_value = value;
		}

		/// <summary>
		/// Gets if the maybe has a value.
		/// </summary>
		/// <returns><c>true</c> when the maybe has a value; otherwise <c>false</c>.</returns>
		public bool HasValue
		{
			get { return _hasValue; }
		}

		/// <summary>
		/// Get the value of the maybe
		/// </summary>
		/// <exception cref="InvalidOperationException">Throw when the Maybe has no value.</exception>
		public TValue Value
		{
			get
			{
				Contract.Requires<InvalidOperationException>(HasValue == true, "Cannot get the value of a maybe that has no value.");
				return _value;
			}
		}

		/// <summary>
		/// Get the value of the maybe.
		/// </summary>
		/// <exception cref="InvalidOperationException">Thrown when the Maybe has no value.</exception>
		object IMaybe.Value
		{
			get { return _value; }
		}

		/// <summary>
		/// Get the underlying type of the maybe.
		/// </summary>
		/// <returns>The type of the maybe.</returns>
		Type IMaybe.GetUnderlyingType()
		{
			return typeof(TValue);
		}

		/// <summary>
		/// Get the value of the maybe, or when the maybe has no value, the provided default value.
		/// </summary>
		/// <param name="defaultValue">The default value to return when the maybe has no value.</param>
		/// <returns>The value of the maybe or the provided default value.</returns>
		public TValue GetValueOrDefault(TValue defaultValue)
		{
			if (_hasValue)
				return _value;

			return defaultValue;
		}

		/// <summary>
		/// Get the value of the maybe, or when the maybe has no value, the provided default value.
		/// </summary>
		/// <param name="valueProvider">The function that will provide the default value when the may has no value of its own.</param>
		/// <returns>The value of the maybe or the provided default value.</returns>
		public TValue GetValueOrDefault(Func<TValue> valueProvider)
		{
			Contract.Requires<ArgumentNullException>(valueProvider != null);

			if (_hasValue)
				return _value;

			return valueProvider();
		}

		#region Comparison

		/// <summary>
		/// Compares this maybe with the given value.
		/// </summary>
		/// <param name="other">The other maybe to compare against.</param>
		/// <returns><c>true</c> when both maybes have a value that are equal, or both are nothing; otherwise <c>false</c>.</returns>
		public bool Equals(Maybe<TValue> other)
		{
			if (_hasValue == false && other._hasValue == false)
				return true;
			if (_hasValue == false || other._hasValue == false)
				return false;
			return EqualityComparer<TValue>.Default.Equals(_value, other._value);
		}

		/// <summary>
		/// Compares this maybe with the given value.
		/// </summary>
		/// <param name="other">The other value to compare against.</param>
		/// <returns><c>true</c> when this maybe has a value that is equal to the given value; otherwise <c>false</c>.</returns>
		public bool Equals(TValue other)
		{
			if (_hasValue == false)
				return false;
			return EqualityComparer<TValue>.Default.Equals(_value, other);
		}

		#endregion
		#region Conversion

		/// <summary>
		/// Implicitly convert the given value to a maybe with the value.
		/// </summary>
		/// <param name="value">The value to decorate with a maybe.</param>
		/// <returns>A maybe with the provided value.</returns>
		public static implicit operator Maybe<TValue>(TValue value)
		{
			return new Maybe<TValue>(value);
		}

		/// <summary>
		/// Explicitly convert the maybe to the value it contains; thus 'unwrapping' the maybe into its bare value.
		/// </summary>
		/// <param name="possibleValue">The maybe to cast/convert to its bare value.</param>
		/// <returns></returns>
		/// <exception cref="InvalidCastException">Throw when the maybe has no value (nothing).</exception>
		public static explicit operator TValue(Maybe<TValue> possibleValue)
		{
			Contract.Requires<InvalidCastException>(possibleValue.HasValue == true);
			return possibleValue.Value;
		}

		// special case implicit case for object maybe
		/// <summary>
		/// implicity convert the maybe to a maybe of type <see cref="object"/>.
		/// </summary>
		/// <param name="derived">The more derived maybe to convert to a object maybe.</param>
		/// <returns>The maybe as a maybe of type object.</returns>
		public static implicit operator Maybe<object>(Maybe<TValue> derived)
		{
			if (!derived.HasValue)
				return new Maybe<object>();

			return new Maybe<object>(derived.Value);
		}

		// explict back cast
		/// <summary>
		/// Explicitly convert the maybe of type object back to a more derived maybe.
		/// </summary>
		/// <param name="objMaybe">The maybe of type object to try to cast to the more derived maybe.</param>
		/// <returns></returns>
		/// <exception cref="InvalidCastException">Throw when the cast failed because the value of the maybe cannot be cast.</exception>
		public static explicit operator Maybe<TValue>(Maybe<object> objMaybe)
		{
			if (!objMaybe.HasValue)
				return new Maybe<TValue>();

			// The cast from obj to TValue might throw InvalidCastException
			// which is exactly what we want if types do not match.
			return new Maybe<TValue>((TValue)objMaybe.Value);
		}

		#endregion
	}
}
